/*
 * EjbFactory.java
 *
 * Created on Oct 8, 2007, 3:29:11 PM
 *
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.drotposta.unitils;

import com.drotposta.unitils.impl.SessionContextImpl;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.ejb.EJB;
import javax.ejb.SessionContext;
import javax.ejb.Stateful;
import javax.ejb.Stateless;
import javax.persistence.EntityTransaction;
import javax.persistence.PersistenceContext;

/**
 *
 * @author merdei
 */
public class EjbFactory<T> {

    private boolean rollbackOnly = false;

    private static Logger logger = Logger.getLogger(EjbFactory.class.getName());

    private T bean;

    private Map<Class<T>, T> beans = new HashMap<Class<T>, T>();

    private SessionContext context;

    private Directory directory;

    Injector emInjector;

    Injector sessionContectInjector;

    /**
     * Container managed transaction.
     */
    private boolean transacted = true;

    public EjbFactory(Class<T> service, Directory directory) {
        this.directory = directory;
        context = new SessionContextImpl(directory);
        emInjector = new EntityManagerInjector(directory);
        sessionContectInjector = new ResourceInjector(context, SessionContext.class);
        bean = createEjb(service);
    }

    public EjbFactory(Class<T> service, Directory directory, boolean transacted) {
        this(service, directory);
        this.transacted = transacted;
    }

    public T getEjb() {
        return bean;
    }

    private T createEjb(Class<T> service) {
        T ejb = createInternalEjb(service);
        if (transacted) {
            T wrappedBean = getAOPWrappedBean(ejb);
            return wrappedBean;
        } else {
            return ejb;
        }
    }

    private T createInternalEjb(Class<T> service) {
        //create the implementation instance
        T bean = createBean(service);
        //save instance reference to local context database
        registerEjb(service, bean);
        //inject EJB references
        addEjbRefs(bean);
        emInjector.inject(bean, PersistenceContext.class);
        sessionContectInjector.inject(bean, Resource.class);

        postConstruct(bean);
        return bean;
    }

    /**
     * Create an instance from the class.
     * @param clazz
     * @return
     */
    private T newInstance(Class<T> clazz) {
        try {
            return clazz.newInstance();
        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Create implementation instance based on an interface.
     * @param service
     * @return
     */
    private T createBean(Class<T> service) {
//        if (service.isAnnotationPresent(Stateless.class) || service.isAnnotationPresent(Stateful.class)) {
//            return newInstance(service);
//        }
        if (service.isInterface() == false) {
            throw new RuntimeException("Trying to mock a non-service: " + service.getName());
        }
        try {
            Class serviceClass = EJBClassRegister.getInstance().getImplementation(service);
            return (T) serviceClass.newInstance();
        } catch (InstantiationException ex) {
            Logger.getLogger(EjbFactory.class.getName()).log(Level.SEVERE, null, ex);
            throw new RuntimeException("Could not initialize bean class for " + service.getName() + ".");
        } catch (IllegalAccessException ex) {
            Logger.getLogger(EjbFactory.class.getName()).log(Level.SEVERE, null, ex);
            throw new RuntimeException("Could not find bean class for " + service.getName() + ".");

        }
    }

    private T registerEjb(Class<T> service, T bean) {
        return beans.put(service, bean);
    }

    private void addEjbRefs(T bean) {
        Class beanClazz = bean.getClass();
        for (Method method : beanClazz.getMethods()) {
            if (method.isAnnotationPresent(EJB.class)) {
                setEjbRef(bean, method);
            }
        }
        for (Field field : beanClazz.getDeclaredFields()) {
            if (field.isAnnotationPresent(EJB.class)) {
                setEjbRef(bean, field);
            }
        }

        beanClazz = bean.getClass().getSuperclass();
        if (beanClazz != null) {
            for (Method method : beanClazz.getMethods()) {
                if (method.isAnnotationPresent(EJB.class)) {
                    setEjbRef(bean, method);
                }
            }
            for (Field field : beanClazz.getDeclaredFields()) {
                if (field.isAnnotationPresent(EJB.class)) {
                    setEjbRef(bean, field);
                }
            }
        }
    }

    private void setEjbRef(T bean, Method setter) {
        Class otherBeanClass = determineBeanClass(setter);
        Object otherBean = lookupBean(otherBeanClass);
        invoke(bean, setter, otherBean);
    }

    private void setEjbRef(T bean, Field field) {
        Class otherBeanClass = determineBeanClass(field);
        Object otherBean = lookupBean(otherBeanClass);
        try {
            field.setAccessible(true);
            field.set(bean, otherBean);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    private Class determineBeanClass(Method setter) {
        EJB ejbRefAnnotation = setter.getAnnotation(EJB.class);
        if (ejbRefAnnotation.beanInterface() != null && Object.class.equals(ejbRefAnnotation.beanInterface()) == false) {
            return ejbRefAnnotation.beanInterface();
        }
        Class otherBeanClass = setter.getParameterTypes()[0];
        return otherBeanClass;
    }

    private Class determineBeanClass(Field field) {
        EJB ejbRefAnnotation = field.getAnnotation(EJB.class);
        if (ejbRefAnnotation.beanInterface() != null && Object.class.equals(ejbRefAnnotation.beanInterface()) == false) {
            return ejbRefAnnotation.beanInterface();
        } else {
            return field.getType();
        }
    }

    private void invoke(T bean, Method method, Object... args) {
        try {
            method.invoke(bean, args);
        } catch (IllegalArgumentException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e.getCause());
        }
    }

    private Object lookupBean(Class otherBeanClass) {
        if (beans.containsKey(otherBeanClass)) {
            return beans.get(otherBeanClass);
        }
        return createInternalEjb(otherBeanClass);
    }

    private T getAOPWrappedBean(T bean) {
        Class[] interfaces = bean.getClass().getInterfaces();
        return (T) Proxy.newProxyInstance(bean.getClass().getClassLoader(), interfaces, new AOPHandler(bean));
    }

    private void postConstruct(T bean) {
        Class beanClazz = bean.getClass();
        for (Method method : beanClazz.getDeclaredMethods()) {
            method.setAccessible(true);
            if (method.isAnnotationPresent(PostConstruct.class)) {
                invoke(bean, method);
            }
        }
    }

    private class AOPHandler implements InvocationHandler {

        private T bean;

        public AOPHandler(T bean) {
            this.bean = bean;
        }

        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            boolean handle = false;
            EntityTransaction tr = directory.getEntityTransaction();
            try {

                //a Required tranzakció kezelést szimuláljuk 

                if (!tr.isActive()) {
                    logger.log(Level.FINE, "transaction begin");
                    ((SessionContextImpl) context).resetRollbackOnly();
                    tr.begin();
                    handle = true;
                }
                try {
                    Object res = method.invoke(bean, args);
                    return res;
                } catch (InvocationTargetException ex) {
                    throw ex.getCause();
                }
            } catch (RuntimeException e) {
                e.printStackTrace();
                if (handle == true) {
                    tr.rollback();
                } else {
                    rollbackOnly = true;

                }
                throw e.getCause();
            } catch (Error e) {
                e.printStackTrace();
                if (handle == true) {
                    tr.rollback();
                } else {
                    rollbackOnly = true;

                }
                throw e.getCause();
            } catch (Exception ex) {
                ex.printStackTrace();
                throw ex;
            } finally {
                if (handle) {
                    logger.log(Level.FINE, "transaction commit + " + rollbackOnly);
                    if (!context.getRollbackOnly()) {
                        tr.commit();
                    } else {
                        tr.rollback();
                    }
                }

            }
        }
    }
}
